package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 聊天室服务端
 */
public class Server {
    /*
     * java.net.ServerSocket
     * 运行在服务端, 主要作用有两个
     * 1.向系统申请服务端口, 客户端可以通过改端口与服务端建立连接
     * 2.监听服务端口, 一旦一个客户端连接了, 就会立即建立一个Socket对象, 并且返回
     */
    private ServerSocket serverSocket;//总机

    //private List<PrintWriter> allOut = new ArrayList<>();//设计集合存放所有客户端输出流, 用于群发
    private Map<String, PrintWriter> allOut = new HashMap<>();

    public Server(){
        try{
            /*
             * 创建一个ServerSocket对象时, 会申请一个端口
             * 如果该端口被其他程序占用, 则会抛出异常:
             * java.net.BindException: Address already in use
             * 解决办法:
             *  1.杀死占用该端口的进程()
             *  2.更换端口号
             */
            System.out.println("正在启动服务器");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务器启动完毕");
        }catch(IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 该方法用于启动服务端
     */
    public void start(){
        try {
            while(true){
                System.out.println("等待客户连接...");
                /*
                 * ServerSocket提供的重要方法
                 * Socket accept(): 用于接受客户端的连接, 并返回一个Socket实例与连接的客户端交互,该方法是一个阻塞王法, 调用后程序会"卡主", 知道一个客户端连接为止
                 */
                Socket socket = serverSocket.accept();//等待一个客户端连接
                System.out.println("一个客户连接了!");
                //启动一个线程来处理该客户端的交互
                ClientHandler handler = new ClientHandler(socket);//创建一个ClientHandler对象
                Thread t = new Thread(handler);//创建一个线程
                t.start();//启动线程
            }
        } catch (IOException e) {
            //throw new RuntimeException(e);//客户端强制终止时(关闭, 拔网线)会报错, 可以将catch内的方法体删除, 不捕获异常, 冷处理
        }
    }

    /**
     * 运行程序
     * */
    public static void main(String[] args) {
        Server server = new Server();//创建一个Server对象
        server.start();//启动服务端
    }

    /**
     * 该线程任务用于负责与指定的客户端交互
     */
    private class ClientHandler implements Runnable{
        private Socket socket;//客户端套接字
        private String host;//记录客户端的IP地址
        private String nickName;//记录客户端昵称
        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();//通过socket获取客户端的IP地址

        }
        public void run() {
            PrintWriter pw = null;
            try {
                //通过socket可以获取到网络输入流, 读取客户端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                /*
                 * 当我们通过br.readLine()方法读取对方法son过来的一行字符串时,
                 * 远端计算机如果调用scoket.close()正常与我们断开连接(进行四次挥手)
                 * 那么readLine()方法会返回null
                 * 若对方是异常中断(强制杀死进程),此时readLine()会抛出异常
                 * 这种情况服务端是没有办法的
                 */
                /*
                // 接受一条消息
                //String message = br.readLine();//读取一行来字远端计算机发送过来的字符串
                //System.out.println("客户端说: " + message);
                 */

                //通过socket获取输出流用于向客户端发送消息
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw, true);

                nickName = br.readLine();//读取客户端发送过来的昵称
                synchronized (allOut) {//向集合中添加元素需要同步机制, 保证多个线程不能同时添加
                    allOut.put(nickName,pw); //将该客户端的你成河输出流存入Map中
                }
                sendMessageToAll("上线了, 当前在线人数: " + allOut.size());//发送消息给所有客户端

                String message;//声明一个字符串变量用于接收客户端发送过来的消息
                while ((message = br.readLine()) != null) {//读取客户端发送过来的多条消息
                    if (message.startsWith("@")){
                        sendMessageToSomeone(message);//调用私聊方法实现私聊
                    }else{
                        sendMessageToAll("说:"+message);//调用群聊方法实现群聊--昵称+[ip地址]+消息
                    }
                }
            } catch (IOException e) {
            } finally {
                //处理客户端下线后的操作, 共3步:
                synchronized (allOut) {//从集合中删除元素需要同步机制, 保证多个线程不能同时删除
                    allOut.remove(pw);//1)将该客户端的输出流从集合中移除
                    allOut.remove(nickName);
                }
                sendMessageToAll("下线了, 当前在线人数: " + allOut.size());//2)通知所有客户端, 该用户下线了
                try {
                    socket.close();//3)关闭socket
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 将消息发送给所有客户端(群聊)
         */
        private void sendMessageToAll(String message){
            System.out.println(nickName+"["+host+"]说"+message);
            synchronized (allOut) {//群发时也要保证多个线程同步处理
                /*
                 * 新循环遍历集合就是迭代器, 因为迭代器要求遍历过程中不可以用集合的方式来增删元素. 因此为了保证遍历正确, 防止其他线程增删, 这里要和其他线程的增删互斥
                 */
                for (PrintWriter o : allOut.values()) {
                    o.println(nickName + "[" + host + "]说" + message);
                }
            }
        }

        /**
         * 将消息发送给指定用户(私聊)
         */
        private void sendMessageToSomeone(String message){
            //message的样子: @范冰冰: 你好!
            if(message.matches("@.+:.+")){//验证私聊格式是否符合
                //提取对方的昵称
                String toNickName = message.substring(1, message.indexOf(":")); //获取分号后的所有内容
                if (allOut.containsKey(toNickName)){//在线
                    PrintWriter pw = allOut.get(toNickName);//根据昵称获取对方客户端的输出流
                    String toMessage = message.substring(message.indexOf(":"));//提取对方要发送的消息
                    pw.println(nickName+"悄悄对你说:"+toMessage);//向@用户发送消息
                }else{//不在线
                    PrintWriter pw = allOut.get(nickName);//根据昵称获取当前客户端的输出流
                    pw.println("该用户["+toNickName+"]不在线");
                }
            }else{//私聊格式不正确, 通知当前客户端格式不正确
                PrintWriter pw = allOut.get(nickName);//根据昵称获取其对应的输出流
                pw.println("私聊格式不正确!");
            }
        }
    }
}
